// Copyright (c) 2025, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

@Tags(['integration4'])
library;

import 'package:build_runner/src/logging/build_log.dart';
import 'package:test/test.dart';

import '../common/common.dart';

const defaultTimeout = Timeout(Duration(minutes: 3));

void main() async {
  test('web compilers', () async {
    final pubspecs = await Pubspecs.load();
    final tester = BuildRunnerTester(pubspecs);

    tester.writeFixturePackage(FixturePackages.copyBuilder());

    tester.writePackage(
      name: 'root_pkg',
      dependencies: [
        'build',
        'build_config',
        'build_daemon',
        'build_modules',
        'build_runner',
        'build_web_compilers',
        'build_test',
        'scratch_space',
      ],
      pathDependencies: ['builder_pkg'],
      files: {
        'build.yaml': r'''
targets:
  $default:
    builders:
      build_web_compilers:entrypoint:
        generate_for:
          - web/main.dart
''',
        'lib/a.dart': '''
  final stringInA = 'string in a.dart';
''',
        'web/main.dart': '''
import 'package:root_pkg/a.dart';

void main() {
  print(stringInA);
}
''',
        'web/unused.dart': '',
      },
    );

    // Initial build AOT.
    await tester.run(
      'root_pkg',
      'dart run build_runner build --output web:build --force-aot',
    );

    // Initial build.
    await tester.run(
      'root_pkg',
      'dart run build_runner build --output web:build',
    );
    expect(
      tester.readFileTree('root_pkg/build')!.keys,
      containsAll([
        'main.dart.js',
        'main.digests',
        'main.dart.ddc_merged_metadata',
        'main.dart.bootstrap.js',
        'main.ddc.js.metadata',
        'main.ddc.js.map',
        'main.ddc.js',
        'main.dart',
        r'packages/$sdk/dev_compiler/amd/require.js',
        r'packages/$sdk/dev_compiler/ddc/ddc_module_loader.js',
        r'packages/$sdk/dev_compiler/web/dart_stack_trace_mapper.js',
      ]),
    );

    // DDC by default. Does not compile submodules into the root module.
    expect(
      tester.read('root_pkg/build/main.dart.js'),
      isNot(contains('// Generated by dart2js')),
    );
    expect(
      tester.read('root_pkg/build/main.dart.js'),
      isNot(contains('string in a.dart')),
    );

    // With dart2js. Does compile into a single file.
    await tester.run(
      'root_pkg',
      'dart run build_runner build --output web:build '
          '--define build_web_compilers:entrypoint=compiler=dart2js',
    );
    expect(
      tester.read('root_pkg/build/main.dart.js'),
      contains('string in a.dart'),
    );
    expect(
      tester.read('root_pkg/build/main.dart.js'),
      contains('// Generated by dart2js'),
    );

    // With dart2js + minify.
    await tester.run(
      'root_pkg',
      'dart run build_runner build --output web:build '
          '--define build_web_compilers:entrypoint=compiler=dart2js '
          '--define build_web_compilers:entrypoint=dart2js_args=["--minify"]',
    );
    expect(
      tester.read('root_pkg/build/main.dart.js'),
      isNot(contains('// Generated by dart2js')),
    );
    expect(
      tester.read('root_pkg/build/main.dart.js'),
      contains('typeof dartMainRunner==="function"'),
    );

    // With dart2wasm.
    await tester.run(
      'root_pkg',
      'dart run build_runner build --output web:build '
          '--define build_web_compilers:entrypoint=compiler=dart2wasm',
    );
    expect(tester.readBytes('root_pkg/build/main.wasm'), isNotNull);

    // Introduce an error, build fails.
    tester.write('root_pkg/lib/a.dart', 'error');
    var output = await tester.run(
      'root_pkg',
      'dart run build_runner build --output web:build',
      expectExitCode: 1,
    );
    expect(output, contains(BuildLog.failurePattern));

    // Stop importing the file with the error, build succeeds.
    tester.write('root_pkg/web/main.dart', 'void main() {}');
    output = await tester.run(
      'root_pkg',
      'dart run build_runner build --output web:build',
    );
    expect(output, contains(BuildLog.successPattern));

    // With dart_source_cleanup unused source is removed.
    expect(tester.read('root_pkg/build/unused.dart'), '');
    await tester.run(
      'root_pkg',
      'dart run build_runner build --output web:build '
          '--define=build_web_compilers:dart_source_cleanup=enabled=true',
    );
    expect(tester.read('root_pkg/build/unused.dart'), null);
  }, timeout: defaultTimeout);

  // TODO(davidmorgan): the remaining tests are integration tests of
  // the web compilers themselves, support testing like this outside the
  // `build_runner` package.
  test('DDC compiled with the Frontend Server', () async {
    final pubspecs = await Pubspecs.load();
    final tester = BuildRunnerTester(pubspecs);

    tester.writeFixturePackage(FixturePackages.copyBuilder());

    tester.writePackage(
      name: 'root_pkg',
      dependencies: [
        'build',
        'build_config',
        'build_daemon',
        'build_modules',
        'build_runner',
        'build_web_compilers',
        'build_test',
        'scratch_space',
      ],
      pathDependencies: ['builder_pkg'],
      files: {
        'build.yaml': r'''
  targets:
    $default:
      builders:
        build_web_compilers:entrypoint:
          generate_for:
            - web/main.dart

  global_options:
    build_web_compilers|sdk_js:
      options:
        web-hot-reload: true
    build_web_compilers|entrypoint:
      options:
        web-hot-reload: true
    build_web_compilers|entrypoint_marker:
      options:
        web-hot-reload: true
    build_web_compilers|ddc:
      options:
        web-hot-reload: true
    build_web_compilers|ddc_modules:
      options:
        web-hot-reload: true
  ''',
        'web/main.dart': '''
  import 'package:root_pkg/a.dart';

  void main() {
    print(helloWorld);
  }
  ''',
        'lib/a.dart': "const helloWorld = 'Hello World!';",
        'web/unused.dart': '',
      },
    );

    // Initial build.
    await tester.run(
      'root_pkg',
      'dart run build_runner build --output web:build',
    );
    expect(
      tester.readFileTree('root_pkg/build')!.keys,
      containsAll([
        'main.dart.js',
        'main.digests',
        'main.dart.ddc_merged_metadata',
        'main.dart.bootstrap.js',
        'main.ddc.js.metadata',
        'main.ddc.js.map',
        'main.ddc.js',
        'main.dart',
        r'packages/$sdk/dev_compiler/amd/require.js',
        r'packages/$sdk/dev_compiler/ddc/ddc_module_loader.js',
        r'packages/$sdk/dev_compiler/web/dart_stack_trace_mapper.js',
      ]),
    );
    // DDC by default. Does not compile submodules into the root module.
    expect(
      tester.read('root_pkg/build/main.dart.js'),
      isNot(contains('// Generated by dart2js')),
    );
    expect(
      tester.read('root_pkg/build/main.dart.js'),
      isNot(contains('Hello World!')),
    );

    // Introduce an error, build fails.
    tester.write('root_pkg/lib/a.dart', 'error');
    var output = await tester.run(
      'root_pkg',
      'dart run build_runner build --output web:build',
      expectExitCode: 1,
    );
    expect(output, contains(BuildLog.failurePattern));

    // Stop importing the file with the error, build succeeds.
    tester.write('root_pkg/web/main.dart', 'void main() {}');
    output = await tester.run(
      'root_pkg',
      'dart run build_runner build --output web:build',
    );
    expect(output, contains(BuildLog.successPattern));

    // With dart_source_cleanup unused source is removed.
    expect(tester.read('root_pkg/build/unused.dart'), '');
    await tester.run(
      'root_pkg',
      'dart run build_runner build --output web:build '
          '--define=build_web_compilers:dart_source_cleanup=enabled=true',
    );
    expect(tester.read('root_pkg/build/unused.dart'), null);
  }, timeout: defaultTimeout);

  test('DDC compiled with the Frontend Server '
      'can recompile incrementally after valid edits', () async {
    final pubspecs = await Pubspecs.load();
    final tester = BuildRunnerTester(pubspecs);

    tester.writeFixturePackage(FixturePackages.copyBuilder());
    tester.writePackage(
      name: 'root_pkg',
      dependencies: [
        'build',
        'build_config',
        'build_daemon',
        'build_modules',
        'build_runner',
        'build_web_compilers',
        'build_test',
        'scratch_space',
      ],
      pathDependencies: ['builder_pkg', 'pkg_a'],
      files: {
        'build.yaml': r'''
  targets:
    $default:
      builders:
        build_web_compilers:entrypoint:
          generate_for:
            - web/**.dart

  global_options:
    build_web_compilers|sdk_js:
      options:
        web-hot-reload: true
    build_web_compilers|entrypoint:
      options:
        web-hot-reload: true
    build_web_compilers|entrypoint_marker:
      options:
        web-hot-reload: true
    build_web_compilers|ddc:
      options:
        web-hot-reload: true
    build_web_compilers|ddc_modules:
      options:
        web-hot-reload: true
  ''',
        'web/main.dart': '''
  import 'package:pkg_a/a.dart';

  void main() {
    print(helloWorld);
  }
  ''',
      },
    );
    tester.writePackage(
      name: 'pkg_a',
      dependencies: ['build_runner'],
      pathDependencies: ['builder_pkg'],
      files: {'lib/a.dart': "String helloWorld = 'Hello World!';"},
    );
    final generatedDirRoot = 'root_pkg/.dart_tool/build/generated';
    final watch = await tester.start('root_pkg', 'dart run build_runner watch');
    await watch.expect(BuildLog.successPattern);
    expect(
      tester.read('$generatedDirRoot/pkg_a/lib/a.ddc.js'),
      contains('Hello World!'),
    );
    expect(
      tester.read('$generatedDirRoot/root_pkg/web/main.ddc.js'),
      isNot(contains('Hello')),
    );

    // Make a simple edit, rebuild succeeds.
    tester.write('pkg_a/lib/a.dart', "String helloWorld = 'Hello Dash!';");
    await watch.expect(BuildLog.successPattern);
    expect(
      tester.read('$generatedDirRoot/pkg_a/lib/a.ddc.js'),
      contains('Hello Dash!'),
    );
    expect(
      tester.read('$generatedDirRoot/root_pkg/web/main.ddc.js'),
      isNot(contains('Hello')),
    );

    // Make another simple edit, rebuild succeeds.
    tester.write('pkg_a/lib/a.dart', "String helloWorld = 'Hello Dart!';");
    await watch.expect(BuildLog.successPattern);
    expect(
      tester.read('$generatedDirRoot/pkg_a/lib/a.ddc.js'),
      contains('Hello Dart!'),
    );
    expect(
      tester.read('$generatedDirRoot/root_pkg/web/main.ddc.js'),
      isNot(contains('Hello')),
    );
  }, timeout: defaultTimeout);

  test('DDC compiled with the Frontend Server '
      'can recompile incrementally after invalid edits', () async {
    final pubspecs = await Pubspecs.load();
    final tester = BuildRunnerTester(pubspecs);

    tester.writeFixturePackage(FixturePackages.copyBuilder());
    tester.writePackage(
      name: 'root_pkg',
      dependencies: [
        'build',
        'build_config',
        'build_daemon',
        'build_modules',
        'build_runner',
        'build_web_compilers',
        'build_test',
        'scratch_space',
      ],
      pathDependencies: ['builder_pkg', 'pkg_a'],
      files: {
        'build.yaml': r'''
  targets:
    $default:
      builders:
        build_web_compilers:entrypoint:
          generate_for:
            - web/**.dart

  global_options:
    build_web_compilers|sdk_js:
      options:
        web-hot-reload: true
    build_web_compilers|entrypoint:
      options:
        web-hot-reload: true
    build_web_compilers|entrypoint_marker:
      options:
        web-hot-reload: true
    build_web_compilers|ddc:
      options:
        web-hot-reload: true
    build_web_compilers|ddc_modules:
      options:
        web-hot-reload: true
  ''',
        'web/main.dart': '''
  import 'package:pkg_a/a.dart';

  void main() {
    print(helloWorld);
  }
  ''',
      },
    );
    tester.writePackage(
      name: 'pkg_a',
      dependencies: ['build_runner'],
      pathDependencies: ['builder_pkg'],
      files: {'lib/a.dart': "String helloWorld = 'Hello World!';"},
    );
    final generatedDirRoot = 'root_pkg/.dart_tool/build/generated';
    final watch = await tester.start('root_pkg', 'dart run build_runner watch');
    await watch.expect(BuildLog.successPattern);
    expect(
      tester.read('$generatedDirRoot/pkg_a/lib/a.ddc.js'),
      contains('Hello World!'),
    );
    expect(
      tester.read('$generatedDirRoot/root_pkg/web/main.ddc.js'),
      isNot(contains('Hello')),
    );

    // Introduce a generic class, rebuild succeeds.
    tester.write('pkg_a/lib/a.dart', '''
class Foo<T, U>{}
String helloWorld = 'Hello Dash!';
''');
    await watch.expect(BuildLog.successPattern);
    expect(
      tester.read('$generatedDirRoot/pkg_a/lib/a.ddc.js'),
      contains('Hello Dash!'),
    );
    expect(
      tester.read('$generatedDirRoot/root_pkg/web/main.ddc.js'),
      isNot(contains('Hello')),
    );

    // Perform an invalid edit (such as changing the number of generic
    // parameters of a class), rebuild succeeds.
    tester.write('pkg_a/lib/a.dart', '''
class Foo<T>{}
String helloWorld = 'Hello Dash!';
''');
    final errorText = await watch.expectAndGetBlock(BuildLog.failurePattern);
    expect(
      errorText,
      contains('Hot reload rejected due to unsupported changes'),
    );

    // Revert the invalid edit, rebuild succeeds.
    tester.write('pkg_a/lib/a.dart', '''
class Foo<T, U>{}
String helloWorld = 'Hello Dash!';
''');
    await watch.expect(BuildLog.successPattern);
    expect(
      tester.read('$generatedDirRoot/pkg_a/lib/a.ddc.js'),
      contains('Hello Dash!'),
    );
    expect(
      tester.read('$generatedDirRoot/root_pkg/web/main.ddc.js'),
      isNot(contains('Hello')),
    );
  }, timeout: defaultTimeout);
}
